Flow 与 Typescript

1.类型系统

请先思考一个问题:你见过没有类型的编程语言吗?答案当然是没有,类型是一门编程语言的基础。这是由于在计算机中,任何数值都是以一组比特简单组成的,但不同类型需要不同的硬件单元参与计算,没有类型,硬件无法区分存储器地址、脚本、字符、整数、以及浮点数。所以任何一门编程语言都有类型系统,只是有强弱之分、静态动态之分。Javascript 就是一门弱类型的、动态检查的语言。

2.为什么需要引入静态类型系统

  • 提高代码的可读性
  • 方便对代码分析
  • 保证重构的可靠性
  • 通常 IDE 能更好地支持
  • 编码阶段可以捕获类型相关错误

可以看出,引入静态类型系统主要是方便代码的维护。

3.Flow vs TypeScript

众所周知,flow 是 React 源码中所使用的静态类型检查器,但是可能并没有自己用过,由于大家对 Typescript 更加熟悉,我通过与它对比,来为大家浅析下 flow 的特点。

3.1 区别

TyepScript:使用方便,工具支持健全。

  • 微软出品,C# 之父 Anders Hejlsberg 参与设计
  • 基于 ES6 语法(也包含是 ES7 / ES8 )
  • 类型是可选的
  • 可以编译出纯净、 简洁的 JavaScript 代码

Flow:健全,没有把运行时异常作为目标。

  • Facebook 出品
  • 是一种静态类型检查器,旨在快速查找 JavaScript 应用程序中的错误
  • 不是编译器,而是检查器
  • 可以很容易地通过 babel 在运行时删除类型注释

3.2 实例

3.2.1 基本

TyepScript:

1
2
3
4
let obj: string;
obj = 'yo';
// Error: Type 'number' is not assignable to type 'string'.
obj = 10;
1
2
3
4
5
// types can be inferred (return type)
function sayIt(what: string) {
return `Saying: ${what}`;
}
const said: string = sayIt(obj);
1
2
3
4
5
6
7
8
9
10
11
12
13
class Sayer {
// mandatory
what: string;

constructor(what: string) {
this.what = what;
}

// return type if you want to
sayIt(): string {
return `Saying: ${this.what}`;
}
}

Flow:

1
2
3
4
let obj: string;
obj = 'yo';
// Error: number: This type is incompatible with string
obj = 10;
1
2
3
4
function sayIt(what: string) {
return `Saying: ${what}`;
}
const said: string = sayIt(obj);
1
2
3
4
5
6
7
8
9
10
11
class Sayer {
what: string;

constructor(what: string) {
this.what = what;
}

sayIt(): string {
return `Saying: ${this.what}`;
}
}

没错,基本功能上来看,两者完全一样

3.2.2 非空类型(Non-Nullable Types)

TyepScript:

1
2
3
4
5
function foo(num: number) {
if (num > 10) {
return 'cool';
}
}
1
2
3
// cool
const result: string = foo(100);
console.log(result.toString());
1
2
// still cool?
console.log(foo(1).toString());
1
2
// error at runtime
"Cannot read property 'toString' of undefined";

TypeScript 不 catch 这个

Flow:

1
2
3
4
5
6
7
8
9
function foo(num: number) {
if (num > 10) {
return 'cool';
}
}

// error: call of method `toString`.
// Method cannot be called on possibly null value
console.log(foo(100).toString());

Flow 可以 catch 这个,为什么呢?
因为 FLow 不会将字符串推断为返回类型

1
2
3
4
5
6
// error: return undefined. This type is incompatible with string
function foo(num: number): string {
if (num > 10) {
return 'cool';
}
}
1
2
3
4
5
6
// nullable type: the one inferred
function foo(num: number): string | void {
if (num > 10) {
return 'cool';
}
}
1
2
3
4
5
// to fix this, we need to check the result
const fooed: string | void = foo(100);
if (fooed) {
fooed.toString();
}

但是 TypeScript 2.x 也可以 catch 这个了,通过开启strictNullChecks
总结一下:

  • 在 TypeScript 1 中类型默认是可空的
  • 在 TypeScript 1.x 中不为空的限制是可能的了
  • 在 Flow 中类型默认是不可空的。
  • 在 TysScript 2.x 中在“TSCONFIG.JSON”中使用strictNullChecks默认类型不可空。
3.2.3 注意
1
2
3
4
5
6
7
class Person {
name: string;
constructor() {
// why no error?
}
}
const olli: Person = new Person();
1
2
// issues error as expected
const daniel: Person = {};

Flow 和 TypeScript 2.0 都不能 catch 这个,但是 TypeScript 中这个不会被修复了。

3.3 泛型

1
2
3
4
5
6
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
1
2
3
4
class Dog extends Animal {
// just to make this different from cat
goodBoyFactor: number;
}
1
2
3
class Cat extends Animal {
purrFactor: number;
}

泛型:类型可以由其他参数化

1
2
3
4
5
6
7
8
9
10
11
12
13
let cats: Array<Cat> = []; // can only contain cats
let animals: Array<Animal> = []; // can only contain animals

// nope, no cat
cats.push(10);

// nope, no cat

// cool, is a cat
cats.push(new Cat('Purry'));

// cool, cat is a sub type of animal
animals.push(new Cat('Purry'));

到目前为止,Flow 和 TypeScript 的工作方式是一样的。但是…

TypeScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let cats: Array<Cat> = []; // can only contain cats
let animals: Array<Animal> = []; // can only contain animals

// error TS2322: Type 'Animal[]' is not assignable to type 'Cat[]'.
// Type 'Animal' is not assignable to type 'Cat'.
// Property 'purrFactor' is missing in type 'Animal'.
cats = animals;

// wow, works, but is no longer safe
animals = cats;

// ouch:
cats.forEach(cat => console.log(`Cat: ${cat.name}`));
// Cat: Purry
// Cat: Brutus
// Cat: Twinky

TypeScript 允许鸟和狗成为猫 :)

Flow:

1
2
3
4
5
6
7
8
9
let cats: Array<Cat> = []; // can only contain cats
let animals: Array<Animal> = []; // can only contain animals

// ERROR
// property `purrFactor` of Cat. Property not found in Animal
cats = animals;

// same ERROR
animals = cats;

但 Flow 不会
这是为什么呢?

  • TypeScript

    • 如果要分配的类型具有更特殊的类型参数,则参数类型是兼容的

    • 似乎最直观

    • 允许明显错误的代码

  • Flow

    • 使用泛型类型,可以从不变量(精确匹配)、协变(更特殊)和逆变(更常见)中进行选择。

    • Flow 中的数组具有不变的参数类型

    • 更有表现力

    • 更难理解

4.时间线

  • 2012-10 ts 诞生
  • 2013-09 ts 0.9 正式版发布
  • 2014-09 ts 1.0
  • 2014-11 flow 诞生
  • 2016-07 React v15.2.0 引入 flow
  • 2016-08 ts 2.0,引入--strictNullChecks,支持严格空检查
  • 2016-11 ts 2.1,支持对象扩展运算符
  • 2017-04 ts 2.3,支持迭代器
  • 2019-06 ts 3.5,支持Omit 辅助 type
  • 2019-10 ts 3.7,支持 Optional Chaining && Nullish Coalescing (可选链式调用”?.”与空值合并运算符”??”)

5.结语

flow 的出现一定程度推动了 js 静态类型检查的流行,让 Javascript 开发者拥抱静态类型系统,也让我们尝到了静态类型的甜头,但最终发现 Typescript 才是真香,其实可以说 flow 是促进了 typescript 的发展。